#include "ObliqueTo3DTiles.h"
#include "ObliqueAnalyst.h"
#include "ObliqueTilingScheme.h"
#include "OCesium/Transforms.h"
#include "CesiumTool.h"
#include "OCMain/Log.h"

using namespace OCPP;
using namespace OCPP::Cesium;

ObliqueTo3DTiles::ObliqueTo3DTiles()
	: _obliqueAnalyst(new ObliqueAnalyst())
	, _obliqueTilingScheme(new ObliqueTilingScheme())
{

}

void ObliqueTo3DTiles::logProcess(uint32 L, uint32 count)
{
	gltfOptions options = _obliqueTilingScheme->getOptionsByLevel(L);
	OCPP_LOG_INFO << "process:(" << count << "/" <<
		_obliqueTilingScheme->numTileNodes << ") level:(" << L << "/" <<
		_obliqueTilingScheme->numTileLevel - 1 << ") imageScaler:(" <<
		str_format("%.3f", options.imageScaler) << ")\n";

	if (_obliqueAnalyst->config().processCallback != nullptr)
	{
		_obliqueAnalyst->config().processCallback(count, _obliqueTilingScheme->numTileNodes);
	}
}

void ObliqueTo3DTiles::analysis()
{
	_obliqueAnalyst->analysis();
}

void ObliqueTo3DTiles::fromAnalysis()
{
	_obliqueAnalyst->from(joinPath(outputPath, "Analysis"));
}

void ObliqueTo3DTiles::setDebugLevel(uint32 value)
{
	_obliqueAnalyst->config().debugDepth = value;
}

inline void fixScale(float& scale, float baseScale)
{
	if (scale < baseScale) scale = baseScale;
	else if (scale < baseScale * 2) scale = baseScale * 2;
	else if (scale < baseScale * 3) scale = baseScale * 3;
	else if (scale < baseScale * 4) scale = baseScale * 4;
	else if (scale < baseScale * 5) scale = baseScale * 5;
	else if (scale < baseScale * 6) scale = baseScale * 6;
	else if (scale < baseScale * 7) scale = baseScale * 7;
	else scale = 1.0;
}

inline void offsetBox(osg::BoundingBox& box, Vector3 offset)
{
	box.xMin() -= offset.x;
	box.xMax() -= offset.x;

	box.yMin() -= offset.y;
	box.yMax() -= offset.y;

	box.zMin() -= offset.z;
	box.zMax() -= offset.z;
}

void ObliqueTo3DTiles::write()
{
	gltfConfig::preferGLB = true;
	bRunning = true;
	uint32 doneNodeCount = 0;

	ObliqueConfig& config = _obliqueAnalyst->config();
	auto maxLevel = config.nodeDepth;
	auto location = config.location;

	OCPP_LOG_INFO << "NodeDepth: " << config.nodeDepth << "\n";
	OCPP_LOG_INFO << "NumTiles: " << config.numTiles << "\n";

	std::vector<gltfOptions> imageScales;
	const static uint32 geSize = 256 * 256;

	float baseScale = maxLevel > 3 ? 0.125 : 0.25;

	for (auto i = 0; i < config.numPixelPerLevels.size(); i++)
	{
		uint32 s = config.getOverlapTileNum(config.startTileLevel + i);
		auto geSize1 = geSize * s;
		auto& pixels = config.numPixelPerLevels[i];
		float scale = 1.0 / (pixels / geSize1) * pow(2, i);
		fixScale(scale, baseScale);
		if (scale >= 1.0 && i > 0)
		{
			float preScale = imageScales[i - 1].imageScaler + baseScale;
			if (preScale < 1.0)
			{
				scale = preScale;
			}
		}
		imageScales.push_back(gltfOptions{ scale });
	}

	imageScales[imageScales.size() - 1].imageScaler = 1.0;

	//output info
	{
		String dir = joinPath(config.dataPath, "../info");
		FileUtil::makeDirectory(dir);
		Json::Value jsonValue;
		for (const auto& config : imageScales)
		{
			jsonValue["imageScaler"].append(config.imageScaler);
		}
		String fileName = joinPath(dir, "output.json");
		writeJsonFile(jsonValue, fileName);
	}


	uint32 scaleL = config.startTileLevel;
	for (auto& scaleConfig : imageScales)
	{
		OCPP_LOG_INFO << str_format("LevelAndScale:(%d\t%.3f)", scaleL++, scaleConfig.imageScaler) << "\n";
	}

	_obliqueTilingScheme->setOptionsPerLevel(imageScales);

	if (config.processLogCallback)
	{
		config.processLogCallback(str_format("Tile Level from %d to %d.\n",
			config.startTileLevel, config.startTileLevel + maxLevel));
		config.processLogCallback("Step(2/3): Split tiles tree.");
	}

	_obliqueTilingScheme->split(*_obliqueAnalyst);

	if (!FileUtil::existFile(outputPath))
	{
		FileUtil::makeDirectory(outputPath);
	}

	String output = joinPath(outputPath, "3dtiles");

	if (!FileUtil::existFile(output))
	{
		FileUtil::makeDirectory(output);
	}

	osg::ref_ptr<TDTILES::Tileset> tileSet = OC_NEW_Tiltset();

	tileSet->root()->transform() = Transforms::eastNorthUpToFixedFrame(
		Cartesian3::fromDegrees(location.x, location.y, location.z));
	tileSet->root()->boundingVolume()->box() = config.boundingBox;

	gltfOptions options = _obliqueTilingScheme->getOptionsByLevel(0);

	double geometricError = config.computeGE(config.startTileLevel);

	tileSet->geometricError() = geometricError;
	tileSet->root()->geometricError() = geometricError;

	if (config.processLogCallback)
	{
		config.processLogCallback(
			str_format("Step(3/3): Write tile glb total num is %d", _obliqueTilingScheme->numTileNodes)
		);
	}

	auto& topTilingLevel = _obliqueTilingScheme->topTilingLevel;

	using ListNextLevel = std::vector<std::tuple<std::shared_ptr<ObliqueTilingLevel>, Cesium::Tile*> >;
	ListNextLevel nextLevels;
	std::vector<std::future<String> > tasks;

	auto writeGLBTask = [this, &config](uint32 L, ObliqueNodeInfoVec nodeInfoVec, String output, String uri, gltfOptions options)
		{
			osg::ref_ptr<osg::Group> group = new osg::Group();
			for (auto& nodeInfo : nodeInfoVec)
			{
				if (nodeInfo.numVertex == 0) continue;
				String fileName = joinPath(nodeInfo.tileID, nodeInfo.fileName);
				osg::ref_ptr<osg::Node> node = osgDB::readNodeFile(joinPath(config.dataPath, fileName));
				group->addChild(node);
			}
			if (group->getNumChildren() == 0)
			{
				return String("error writing ") + uri + "\n";
			}

			options.logStream = std::make_shared<StringStream>();
			bool ret = CesiumTool::writeGLB(*group, joinPath(output, uri), options);

			if (!ret)
			{
				return String("error writing ") + uri + "\n";
			}
			return uri + " " + ((StringStream*)options.logStream.get())->str();
		};

	auto waitForWrite = [this, &config, &tasks, &doneNodeCount](size_t numAsync, uint32 L)
		{
			if (tasks.size() >= numAsync)
			{
				for (auto& t : tasks)
				{
					String message = t.get();
					if (StringUtil::startsWith(message, "error"))
					{
						if (config.processLogCallback)
						{
							config.processLogCallback(message);
						}
						OCPP_LOG_ERROR << message << "\n";
					}
					else
					{
						OCPP_LOG_INFO << message << "\n";
					}
					logProcess(L, ++doneNodeCount);
				}
				tasks.clear();
			}
		};

	uint32 L = 0;

	auto genUri = [&config](ObliqueTilingLevel* tileLevel, uint32 i)
		{
			String nodePath = tileLevel->getNodePath();
			if (nodePath.empty())
			{
				uint32 L = config.startTileLevel + tileLevel->level;
				String uri = str_format("%d_%d.glb", L, i);
				return uri;
			}
			else
			{
				//String preFix = nodePath.substr(nodePath.find_last_of('/') + 1, nodePath.length());
				String uri = joinPath(nodePath, str_format("%d.glb", i));
				return uri;
			}
		};

	for (auto i = 0; i < topTilingLevel->boxGroupVec.size(); i++)
	{
		if (!bRunning) break;
		auto& tileBox = topTilingLevel->boxGroupVec[i];

		String uri = genUri(topTilingLevel.get(), i);
		String glbFileName = joinPath(output, uri);

		if (rewrite || !FileUtil::existFile(glbFileName))
		{
			gltfOptions options = _obliqueTilingScheme->getOptionsByLevel(0);
			tasks.push_back(std::async(writeGLBTask, 0, tileBox.tiles, output, uri, options));
		}
		else
		{
			logProcess(0, ++doneNodeCount);
		}

		auto tile = new Cesium::Tile;

		tile->geometricError() = geometricError * pow(0.5, L + 1);
		tile->boundingVolume()->box() = tileBox.box;
		tile->content()->uri() = uri;
		tileSet->root()->children().push_back(tile);

		if (tileBox.nextLevel)
		{
			nextLevels.push_back({ tileBox.nextLevel ,tile });
		}
		else
		{
			tile->geometricError() = 0.0;
		}
	}

	waitForWrite(config.numThread, 0);
	L++;
	while (nextLevels.size() > 0)
	{
		if (!bRunning) break;
		ListNextLevel tmpListNext;

		for (auto& [level, parentTile] : nextLevels)
		{
			if (!bRunning) break;
			for (auto i = 0; i < level->boxGroupVec.size(); i++)
			{
				if (!bRunning) break;
				auto& tileBox = level->boxGroupVec[i];

				String subPath = joinPath(output, level->getNodePath());
				if (!FileUtil::existFile(subPath))
				{
					FileUtil::makeDirectory(subPath);
				}

				for (auto& noChildNode : level->noChildNodeVec)
				{
					auto uri = joinPath(level->getNodePath(),
						StringUtil::replaceAll(noChildNode.fileName, ".osgb", ".glb"));
					auto tile = new Cesium::Tile;
					tile->geometricError() = 0.0;
					tile->boundingVolume()->box() = noChildNode.boundingBox;
					tile->content()->uri() = uri;
					parentTile->children().push_back(tile);
					String glbFileName = joinPath(output, uri);
					if (rewrite || !FileUtil::existFile(glbFileName))
					{
						doneNodeCount--;
						tasks.push_back(std::async(writeGLBTask, L,
							ObliqueNodeInfoVec{ noChildNode }, output, uri, gltfOptions()));
					}
				}

				//
				if (tileBox.tiles.size() > 0)
				{
					String uri = genUri(level.get(), i);
					String glbFileName = joinPath(output, uri);

					if (rewrite || !FileUtil::existFile(glbFileName))
					{
						auto options = _obliqueTilingScheme->getOptionsByLevel(L);
						tasks.push_back(std::async(writeGLBTask, L, tileBox.tiles, output, uri, options));
					}
					else
					{
						logProcess(L, ++doneNodeCount);
					}

					auto tile = new Cesium::Tile;
					auto geometricError1 = geometricError * pow(0.5, L + 1);

					tile->geometricError() = geometricError1;

					tile->boundingVolume()->box() = tileBox.box;
					tile->content()->uri() = uri;
					parentTile->children().push_back(tile);

					if (tileBox.nextLevel && L <= config.debugDepth - 1)
					{
						tmpListNext.push_back({ tileBox.nextLevel ,tile });
					}
					else
					{
						tile->geometricError() = 0.0;
					}
				}
				else
				{
					doneNodeCount++;
				}
			}
			waitForWrite(config.numThread, L);
		}

		nextLevels = std::move(tmpListNext);
		L++;
	}

	waitForWrite(0, L - 1);

	OC_WRITE_Tiltset(tileSet, joinPath(output, "tileset.json"));

	bRunning = false;

	if (_obliqueAnalyst->config().finishedCallback != nullptr)
	{
		_obliqueAnalyst->config().finishedCallback();
	}

	OCPP_LOG_INFO << "Done:" << output << "\n";
}

bool ObliqueTo3DTiles::isRunning()
{
	return bRunning;
}

void ObliqueTo3DTiles::stop()
{
	bRunning = false;
}

void ObliqueTo3DTiles::setDataPath(String value)
{
	_obliqueAnalyst->config().dataPath = value;
	_obliqueAnalyst->touch();
}

void ObliqueTo3DTiles::setOutputPath(String value)
{
	outputPath = value;
	_obliqueAnalyst->config().outputPath = value;
}

void ObliqueTo3DTiles::setProcessCallback(ProcessCallback callback)
{
	_obliqueAnalyst->config().processCallback = callback;
}
void ObliqueTo3DTiles::setFinishedCallback(FinishedCallback callback)
{
	_obliqueAnalyst->config().finishedCallback = callback;
}

void ObliqueTo3DTiles::setNumThread(uint32 value)
{
	_obliqueAnalyst->config().numThread = value;
}

void ObliqueTo3DTiles::setProcessLogCallback(ProcessLogCallback value)
{
	_obliqueAnalyst->config().processLogCallback = value;
}
